/* file name  : JSlingServer.java
 * authors    : Saikat Guha <saikat@cs.cornell.edu>
 * created    : Fri 23 Sep 2005 10:26:12 PM EDT
 * copyright  : GNU Lesser General Public License version 2.1 or later
 *
 * Copyright (C) 2005 Saikat Guha <saikat@cs.cornell.edu>
 *
 * This library is free software; you can redistribute it and/or modify it under the terms
 * of the GNU Lesser General Public License as published by the Free Software Foundation;
 * either version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 *  without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License along with this
 * library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330,
 * Boston, MA 02111-1307 USA
 *
 */
package net.nutss.signaling;

import java.net.*;
import java.util.HashMap;
import java.util.Date;
import java.io.*;

import net.nutss.Control;
import static net.nutss.Control.log;
import static net.nutss.signaling.JSlingMessage.Type.*;
import static net.nutss.signaling.JSlingMessage.Response.*;

import net.nutss.stunt.STUNTServer;

/**
* Implements a directory server and signaling proxy. Clients register with the directory by sending a HELLO message. Clients can
* request a packet to be forwarded to another client by sending a ROUTE message.
* between clients.
*
* @author Saikat Guha
* @version 0.1
 */
public class JSlingServer extends Thread {

    /**
    * Timeout for ID registrations in seconds
     */
    static final int TIMEOUT = 300;

    /**
    *Stores client to address maps, and other details.
     */
    private HashMap<URI,DirectoryEntry> directory = null;

    /**
    *UDP socket for proxy communication.
     */
    private DatagramSocket server;

    /**
    * STUNT Responders
     */
    private SocketAddress[] responders;

    /**
    *Client record
    *
    * @author Saikat Guha
    * @version 0.1
     */
    private class DirectoryEntry {

        /**
        *Client'd ID. e.g. user@nutss.net
         */
        URI id;

        /**
        *Client's IP address and UDP port where the server can send messages.
         */
        InetSocketAddress address;

        /**
        *Time at which the entry expires. By default, 5 minutes after the last refresh message.
         */
        Date expires;

        /**
        * Constructs a new client entry.
        *
        * @param id Client ID
        * @param address Client's UDP address and port
        * @param expires Expiry time
         */
        DirectoryEntry(URI id, InetSocketAddress address, Date expires) {
            this.id = id;
            this.address = address;
            this.expires = expires;
        }
    }

    /**
    * Creates a new proxy server
    *
    * @param port UDP port on which to listen
    * @param addrs IP address and TCP ports of STUNT Responders
    * @throws IOException When server cannot listen on given port
     */
    public JSlingServer(int port, SocketAddress[] addrs) throws IOException {
        directory = new HashMap<URI,DirectoryEntry>();
        server = new DatagramSocket(port);
        responders = addrs;
    }

    /**
    * Returns the local IP and port the server is listening on.
    * @return Server socket address
     */
    private SocketAddress getSocketAddress() {
        return server.getLocalSocketAddress();
    }


    /**
    * Starts the proxy server.
     */
    public void run() {
        SocketAddress addr;
        DatagramPacket pkt = new DatagramPacket(new byte[1472], 1472);
        ByteArrayOutputStream bos;
        ObjectOutputStream oos;
        DirectoryEntry src, dst;
        Date expires;
        JSlingMessage msg;

        try {
            bos = new ByteArrayOutputStream(1472);
            bos.reset();
        } catch (Exception e) {
            e.printStackTrace();
            return;
        }

        try {
            while (true) {
                server.receive(pkt);
                if (pkt.getLength() == 0) continue;

                /* Read the request */
                try {
                    ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(pkt.getData()));
                    msg = (JSlingMessage) ois.readObject();
                } catch (Exception e) {
                    log.throwing("JSlllingServer", "run", e);
                    continue;
                }

                addr = pkt.getSocketAddress();

                log.finest(String.format("RECEIVED: addr=%s size=%d msg=%s", addr, pkt.getLength(), msg));

                switch(msg.type) {

                    /* Client registration request. Create a client entry or update expiry timers. */
                    case JSLING_REGISTER:
                        src = directory.get(msg.source);
                        expires = new Date(TIMEOUT*1000 + System.currentTimeMillis());
                        if (src == null || src.expires.before(new Date())) {
                            // Allow client to register new or expired ID
                            directory.put(msg.source, new DirectoryEntry(msg.source, (InetSocketAddress) pkt.getSocketAddress(), expires));
                            System.out.println(msg.source + " is " + pkt.getSocketAddress());
                            msg.response = JSLING_SUCCESS;
                            msg.payload = responders;
                        } else if (src.address.getAddress().equals(pkt.getAddress())) {
                            // Allow client to refresh own registration
                            // as long as IP address matches
                            src.expires = expires;
                            if (!src.address.equals(pkt.getSocketAddress()))
                                System.out.println(msg.source + " now is " + pkt.getSocketAddress());
                            src.address = (InetSocketAddress) pkt.getSocketAddress();
                            msg.response = JSLING_SUCCESS;
                            msg.payload = responders;
                        } else {
                            // Prevent clients from pre-empting IDs from different IPs
                            msg.response = JSLING_ERROR;
                        }
                        break;

                    /* Forward to destination in one hop. Return error otherwise. */
                    case JSLING_FORWARD:
                        dst = directory.get(msg.response == null ? msg.destination : msg.source);
                        if (dst == null || dst.expires.before(new Date())) {
                            // No loops, if not locally found - report error
                            msg.response = JSLING_ERROR;
                        } else {
                            // one-hop forward to destination
                            addr = dst.address;
                        }
                        break;

                    /* Route to destination (maybe one hop). Error if sender is not registered. */
                    case JSLING_ROUTE:
                        src = directory.get(msg.response == null ? msg.source : msg.destination);
                        dst = directory.get(msg.response == null ? msg.destination : msg.source);
                        if (src == null || !src.address.equals(pkt.getSocketAddress()) || src.expires.before(new Date())) {
                            // only allow registered sources to send multi-hop packets
                            msg.response = JSLING_ERROR;
                        } else if (dst == null || dst.expires.before(new Date())) {
                            // find authoritative proxy for destination
                            try {
                                addr = JSlingCommon.resolveProxy(msg.destination);
                            } catch (UnknownHostException e) {
                                addr = null;
                            }

                            if (addr == null) {
                                addr = pkt.getSocketAddress();
                                msg.response = JSLING_ERROR;
                            } else {
                                // trapezoid routing. prevent loops
                                msg.type = JSLING_FORWARD;
                            }
                        } else {
                            // found in local table
                            addr = dst.address;
                        }
                        break;

                    default:
                        msg.response = JSLING_ERROR;
                }

                /* Send packet. */
                bos.reset();
                oos = new ObjectOutputStream(bos);
                oos.writeObject(msg);
                oos.flush();
                server.send(new DatagramPacket(bos.toByteArray(), bos.size(), addr));
                log.finest(String.format("SENT: addr=%s size=%d msg=%s", addr, bos.size(), msg));

            }
        } catch (Exception e) {
            log.throwing("JSlingServer", "run", e);
        } finally {
            try { server.close(); } catch (Exception e) { }
        }
    }


    /**
    *Starts the server
    *
    * @param args Program arguments
     */
    public static void main(String args[]) {
        STUNTServer r1, r2;
        JSlingServer p;

        if (args.length != 2) {
            System.err.printf("Usage:\n\tjava JSlingServer ip1 ip2\n" +
                    "The two IP Addresses should be different and belong to local interfaces.\n");
            System.exit(1);
        }

        try {
            //Control.logToFile("JSlingServerLog.txt");
            r1 = new STUNTServer(InetAddress.getByName(args[0]), JSlingCommon.proxyPort+16+(int)(10*Math.random()));
            r2 = new STUNTServer(InetAddress.getByName(args[1]), JSlingCommon.proxyPort+32+(int)(10*Math.random()));
            SocketAddress[] addrs = {r1.getSocketAddress(), r2.getSocketAddress()};
            p = new JSlingServer(JSlingCommon.proxyPort, addrs);

            System.out.printf("STUNT Server 1: %s\n", r1.getSocketAddress());
            System.out.printf("STUNT Server 2: %s\n", r2.getSocketAddress());
            System.out.printf("Directory  : %s\n", p.getSocketAddress());

            r1.start();
            r2.start();
            p.start();
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}
